/**********
This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the
Free Software Foundation; either version 3 of the License, or (at your
option) any later version. (See <http://www.gnu.org/copyleft/lesser.html>.)

This library is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
more details.

You should have received a copy of the GNU Lesser General Public License
along with this library; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
**********/
// "liveMedia"
// Copyright (c) 1996-2018 Live Networks, Inc.  All rights reserved.
// A sink that generates an AVI file from a composite media session
// Implementation

#include "include/AVIFileSink.hh"
#include "include/InputFile.hh"
#include "include/OutputFile.hh"
#include "../groupsock/include/GroupsockHelper.hh"

#define fourChar(x, y, z, w) ( ((w)<<24)|((z)<<16)|((y)<<8)|(x) )/*little-endian*/

#define AVIIF_LIST        0x00000001
#define AVIIF_KEYFRAME        0x00000010
#define AVIIF_NO_TIME        0x00000100
#define AVIIF_COMPRESSOR    0x0FFF0000

////////// AVISubsessionIOState ///////////
// A structure used to represent the I/O state of each input 'subsession':

class SubsessionBuffer {
public:
    SubsessionBuffer(unsigned bufferSize)
            : fBufferSize(bufferSize) {
        reset();
        fData = new unsigned char[bufferSize];
    }

    virtual ~SubsessionBuffer() { delete[] fData; }

    void reset() { fBytesInUse = 0; }

    void addBytes(unsigned numBytes) { fBytesInUse += numBytes; }

    unsigned char *dataStart() { return &fData[0]; }

    unsigned char *dataEnd() { return &fData[fBytesInUse]; }

    unsigned bytesInUse() const { return fBytesInUse; }

    unsigned bytesAvailable() const { return fBufferSize - fBytesInUse; }

    void setPresentationTime(struct timeval const &presentationTime) {
        fPresentationTime = presentationTime;
    }

    struct timeval const &presentationTime() const { return fPresentationTime; }

private:
    unsigned fBufferSize;
    struct timeval fPresentationTime;
    unsigned char *fData;
    unsigned fBytesInUse;
};

class AVISubsessionIOState {
public:
    AVISubsessionIOState(AVIFileSink &sink, MediaSubsession &subsession);

    virtual ~AVISubsessionIOState();

    void setAVIstate(unsigned subsessionIndex);

    void setFinalAVIstate();

    void afterGettingFrame(unsigned packetDataSize,
                           struct timeval presentationTime);

    void onSourceClosure();

    UsageEnvironment &envir() const { return fOurSink.envir(); }

public:
    SubsessionBuffer *fBuffer, *fPrevBuffer;
    AVIFileSink &fOurSink;
    MediaSubsession &fOurSubsession;

    unsigned short fLastPacketRTPSeqNum;
    Boolean fOurSourceIsActive;
    struct timeval fPrevPresentationTime;
    unsigned fMaxBytesPerSecond;
    Boolean fIsVideo, fIsAudio, fIsByteSwappedAudio;
    unsigned fAVISubsessionTag;
    unsigned fAVICodecHandlerType;
    unsigned fAVISamplingFrequency; // for audio
    u_int16_t fWAVCodecTag; // for audio
    unsigned fAVIScale;
    unsigned fAVIRate;
    unsigned fAVISize;
    unsigned fNumFrames;
    unsigned fSTRHFrameCountPosition;

private:
    void useFrame(SubsessionBuffer &buffer);
};


///////// AVIIndexRecord definition & implementation //////////

class AVIIndexRecord {
public:
    AVIIndexRecord(unsigned chunkId, unsigned flags, unsigned offset, unsigned size)
            : fNext(NULL), fChunkId(chunkId), fFlags(flags), fOffset(offset), fSize(size) {
    }

    AVIIndexRecord *&next() { return fNext; }

    unsigned chunkId() const { return fChunkId; }

    unsigned flags() const { return fFlags; }

    unsigned offset() const { return fOffset; }

    unsigned size() const { return fSize; }

private:
    AVIIndexRecord *fNext;
    unsigned fChunkId;
    unsigned fFlags;
    unsigned fOffset;
    unsigned fSize;
};


////////// AVIFileSink implementation //////////

AVIFileSink::AVIFileSink(UsageEnvironment &env,
                         MediaSession &inputSession,
                         char const *outputFileName,
                         unsigned bufferSize,
                         unsigned short movieWidth, unsigned short movieHeight,
                         unsigned movieFPS, Boolean packetLossCompensate)
        : Medium(env), fInputSession(inputSession),
          fIndexRecordsHead(NULL), fIndexRecordsTail(NULL), fNumIndexRecords(0),
          fBufferSize(bufferSize), fPacketLossCompensate(packetLossCompensate),
          fAreCurrentlyBeingPlayed(False), fNumSubsessions(0), fNumBytesWritten(0),
          fHaveCompletedOutputFile(False),
          fMovieWidth(movieWidth), fMovieHeight(movieHeight), fMovieFPS(movieFPS) {
    fOutFid = OpenOutputFile(env, outputFileName);
    if (fOutFid == NULL) return;

    // Set up I/O state for each input subsession:
    MediaSubsessionIterator iter(fInputSession);
    MediaSubsession *subsession;
    while ((subsession = iter.next()) != NULL) {
        // Ignore subsessions without a data source:
        FramedSource *subsessionSource = subsession->readSource();
        if (subsessionSource == NULL) continue;

        // If "subsession's" SDP description specified screen dimension
        // or frame rate parameters, then use these.
        if (subsession->videoWidth() != 0) {
            fMovieWidth = subsession->videoWidth();
        }
        if (subsession->videoHeight() != 0) {
            fMovieHeight = subsession->videoHeight();
        }
        if (subsession->videoFPS() != 0) {
            fMovieFPS = subsession->videoFPS();
        }

        AVISubsessionIOState *ioState
                = new AVISubsessionIOState(*this, *subsession);
        subsession->miscPtr = (void *) ioState;

        // Also set a 'BYE' handler for this subsession's RTCP instance:
        if (subsession->rtcpInstance() != NULL) {
            subsession->rtcpInstance()->setByeHandler(onRTCPBye, ioState);
        }

        ++fNumSubsessions;
    }

    // Begin by writing an AVI header:
    addFileHeader_AVI();
}

AVIFileSink::~AVIFileSink() {
    completeOutputFile();

    // Then, stop streaming and delete each active "AVISubsessionIOState":
    MediaSubsessionIterator iter(fInputSession);
    MediaSubsession *subsession;
    while ((subsession = iter.next()) != NULL) {
        if (subsession->readSource() != NULL) subsession->readSource()->stopGettingFrames();

        AVISubsessionIOState *ioState
                = (AVISubsessionIOState *) (subsession->miscPtr);
        if (ioState == NULL) continue;

        delete ioState;
    }

    // Then, delete the index records:
    AVIIndexRecord *cur = fIndexRecordsHead;
    while (cur != NULL) {
        AVIIndexRecord *next = cur->next();
        delete cur;
        cur = next;
    }

    // Finally, close our output file:
    CloseOutputFile(fOutFid);
}

AVIFileSink *AVIFileSink
::createNew(UsageEnvironment &env, MediaSession &inputSession,
            char const *outputFileName,
            unsigned bufferSize,
            unsigned short movieWidth, unsigned short movieHeight,
            unsigned movieFPS, Boolean packetLossCompensate) {
    AVIFileSink *newSink =
            new AVIFileSink(env, inputSession, outputFileName, bufferSize,
                            movieWidth, movieHeight, movieFPS, packetLossCompensate);
    if (newSink == NULL || newSink->fOutFid == NULL) {
        Medium::close(newSink);
        return NULL;
    }

    return newSink;
}

Boolean AVIFileSink::startPlaying(afterPlayingFunc *afterFunc,
                                  void *afterClientData) {
    // Make sure we're not already being played:
    if (fAreCurrentlyBeingPlayed) {
        envir().setResultMsg("This sink has already been played");
        return False;
    }

    fAreCurrentlyBeingPlayed = True;
    fAfterFunc = afterFunc;
    fAfterClientData = afterClientData;

    return continuePlaying();
}

Boolean AVIFileSink::continuePlaying() {
    // Run through each of our input session's 'subsessions',
    // asking for a frame from each one:
    Boolean haveActiveSubsessions = False;
    MediaSubsessionIterator iter(fInputSession);
    MediaSubsession *subsession;
    while ((subsession = iter.next()) != NULL) {
        FramedSource *subsessionSource = subsession->readSource();
        if (subsessionSource == NULL) continue;

        if (subsessionSource->isCurrentlyAwaitingData()) continue;

        AVISubsessionIOState *ioState
                = (AVISubsessionIOState *) (subsession->miscPtr);
        if (ioState == NULL) continue;

        haveActiveSubsessions = True;
        unsigned char *toPtr = ioState->fBuffer->dataEnd();
        unsigned toSize = ioState->fBuffer->bytesAvailable();
        subsessionSource->getNextFrame(toPtr, toSize,
                                       afterGettingFrame, ioState,
                                       onSourceClosure, ioState);
    }
    if (!haveActiveSubsessions) {
        envir().setResultMsg("No subsessions are currently active");
        return False;
    }

    return True;
}

void AVIFileSink
::afterGettingFrame(void *clientData, unsigned packetDataSize,
                    unsigned numTruncatedBytes,
                    struct timeval presentationTime,
                    unsigned /*durationInMicroseconds*/) {
    AVISubsessionIOState *ioState = (AVISubsessionIOState *) clientData;
    if (numTruncatedBytes > 0) {
        ioState->envir()
                << "AVIFileSink::afterGettingFrame(): The input frame data was too large for our buffer.  "
                << numTruncatedBytes
                << " bytes of trailing data was dropped!  Correct this by increasing the \"bufferSize\" parameter in the \"createNew()\" call.\n";
    }
    ioState->afterGettingFrame(packetDataSize, presentationTime);
}

void AVIFileSink::onSourceClosure(void *clientData) {
    AVISubsessionIOState *ioState = (AVISubsessionIOState *) clientData;
    ioState->onSourceClosure();
}

void AVIFileSink::onSourceClosure1() {
    // Check whether *all* of the subsession sources have closed.
    // If not, do nothing for now:
    MediaSubsessionIterator iter(fInputSession);
    MediaSubsession *subsession;
    while ((subsession = iter.next()) != NULL) {
        AVISubsessionIOState *ioState
                = (AVISubsessionIOState *) (subsession->miscPtr);
        if (ioState == NULL) continue;

        if (ioState->fOurSourceIsActive) return; // this source hasn't closed
    }

    completeOutputFile();

    // Call our specified 'after' function:
    if (fAfterFunc != NULL) {
        (*fAfterFunc)(fAfterClientData);
    }
}

void AVIFileSink::onRTCPBye(void *clientData) {
    AVISubsessionIOState *ioState = (AVISubsessionIOState *) clientData;

    struct timeval timeNow;
    gettimeofday(&timeNow, NULL);
    unsigned secsDiff
            = timeNow.tv_sec - ioState->fOurSink.fStartTime.tv_sec;

    MediaSubsession &subsession = ioState->fOurSubsession;
    ioState->envir() << "Received RTCP \"BYE\" on \""
                     << subsession.mediumName()
                     << "/" << subsession.codecName()
                     << "\" subsession (after "
                     << secsDiff << " seconds)\n";

    // Handle the reception of a RTCP "BYE" as if the source had closed:
    ioState->onSourceClosure();
}

void AVIFileSink::addIndexRecord(AVIIndexRecord *newIndexRecord) {
    if (fIndexRecordsHead == NULL) {
        fIndexRecordsHead = newIndexRecord;
    } else {
        fIndexRecordsTail->next() = newIndexRecord;
    }
    fIndexRecordsTail = newIndexRecord;
    ++fNumIndexRecords;
}

void AVIFileSink::completeOutputFile() {
    if (fHaveCompletedOutputFile || fOutFid == NULL) return;

    // Update various AVI 'size' fields to take account of the codec data that
    // we've now written to the file:
    unsigned maxBytesPerSecond = 0;
    unsigned numVideoFrames = 0;
    unsigned numAudioFrames = 0;

    //// Subsession-specific fields:
    MediaSubsessionIterator iter(fInputSession);
    MediaSubsession *subsession;
    while ((subsession = iter.next()) != NULL) {
        AVISubsessionIOState *ioState
                = (AVISubsessionIOState *) (subsession->miscPtr);
        if (ioState == NULL) continue;

        maxBytesPerSecond += ioState->fMaxBytesPerSecond;

        setWord(ioState->fSTRHFrameCountPosition, ioState->fNumFrames);
        if (ioState->fIsVideo) numVideoFrames = ioState->fNumFrames;
        else if (ioState->fIsAudio) numAudioFrames = ioState->fNumFrames;
    }

    //// Global fields:
    add4ByteString("idx1");
    addWord(fNumIndexRecords * 4 * 4); // the size of all of the index records, which come next:
    for (AVIIndexRecord *indexRecord = fIndexRecordsHead;
         indexRecord != NULL; indexRecord = indexRecord->next()) {
        addWord(indexRecord->chunkId());
        addWord(indexRecord->flags());
        addWord(indexRecord->offset());
        addWord(indexRecord->size());
    }

    fRIFFSizeValue += fNumBytesWritten + fNumIndexRecords * 4 * 4 - 4;
    setWord(fRIFFSizePosition, fRIFFSizeValue);

    setWord(fAVIHMaxBytesPerSecondPosition, maxBytesPerSecond);
    setWord(fAVIHFrameCountPosition,
            numVideoFrames > 0 ? numVideoFrames : numAudioFrames);

    fMoviSizeValue += fNumBytesWritten;
    setWord(fMoviSizePosition, fMoviSizeValue);

    // We're done:
    fHaveCompletedOutputFile = True;
}


////////// AVISubsessionIOState implementation ///////////

AVISubsessionIOState::AVISubsessionIOState(AVIFileSink &sink,
                                           MediaSubsession &subsession)
        : fOurSink(sink), fOurSubsession(subsession),
          fMaxBytesPerSecond(0), fIsVideo(False), fIsAudio(False), fIsByteSwappedAudio(False),
          fNumFrames(0) {
    fBuffer = new SubsessionBuffer(fOurSink.fBufferSize);
    fPrevBuffer = sink.fPacketLossCompensate
                  ? new SubsessionBuffer(fOurSink.fBufferSize) : NULL;

    FramedSource *subsessionSource = subsession.readSource();
    fOurSourceIsActive = subsessionSource != NULL;

    fPrevPresentationTime.tv_sec = 0;
    fPrevPresentationTime.tv_usec = 0;
}

AVISubsessionIOState::~AVISubsessionIOState() {
    delete fBuffer;
    delete fPrevBuffer;
}

void AVISubsessionIOState::setAVIstate(unsigned subsessionIndex) {
    fIsVideo = strcmp(fOurSubsession.mediumName(), "video") == 0;
    fIsAudio = strcmp(fOurSubsession.mediumName(), "audio") == 0;

    if (fIsVideo) {
        fAVISubsessionTag
                = fourChar('0' + subsessionIndex / 10, '0' + subsessionIndex % 10, 'd', 'c');
        if (strcmp(fOurSubsession.codecName(), "JPEG") == 0) {
            fAVICodecHandlerType = fourChar('m', 'j', 'p', 'g');
        } else if (strcmp(fOurSubsession.codecName(), "MP4V-ES") == 0) {
            fAVICodecHandlerType = fourChar('D', 'I', 'V', 'X');
        } else if (strcmp(fOurSubsession.codecName(), "MPV") == 0) {
            fAVICodecHandlerType = fourChar('m', 'p', 'g', '1'); // what about MPEG-2?
        } else if (strcmp(fOurSubsession.codecName(), "H263-1998") == 0 ||
                   strcmp(fOurSubsession.codecName(), "H263-2000") == 0) {
            fAVICodecHandlerType = fourChar('H', '2', '6', '3');
        } else if (strcmp(fOurSubsession.codecName(), "H264") == 0) {
            fAVICodecHandlerType = fourChar('H', '2', '6', '4');
        } else {
            fAVICodecHandlerType = fourChar('?', '?', '?', '?');
        }
        fAVIScale = 1; // ??? #####
        fAVIRate = fOurSink.fMovieFPS; // ??? #####
        fAVISize = fOurSink.fMovieWidth * fOurSink.fMovieHeight * 3; // ??? #####
    } else if (fIsAudio) {
        fIsByteSwappedAudio = False; // by default
        fAVISubsessionTag
                = fourChar('0' + subsessionIndex / 10, '0' + subsessionIndex % 10, 'w', 'b');
        fAVICodecHandlerType = 1; // ??? ####
        unsigned numChannels = fOurSubsession.numChannels();
        fAVISamplingFrequency = fOurSubsession.rtpTimestampFrequency(); // default
        if (strcmp(fOurSubsession.codecName(), "L16") == 0) {
            fIsByteSwappedAudio = True; // need to byte-swap data before writing it
            fWAVCodecTag = 0x0001;
            fAVIScale = fAVISize = 2 * numChannels; // 2 bytes/sample
            fAVIRate = fAVISize * fAVISamplingFrequency;
        } else if (strcmp(fOurSubsession.codecName(), "L8") == 0) {
            fWAVCodecTag = 0x0001;
            fAVIScale = fAVISize = numChannels; // 1 byte/sample
            fAVIRate = fAVISize * fAVISamplingFrequency;
        } else if (strcmp(fOurSubsession.codecName(), "PCMA") == 0) {
            fWAVCodecTag = 0x0006;
            fAVIScale = fAVISize = numChannels; // 1 byte/sample
            fAVIRate = fAVISize * fAVISamplingFrequency;
        } else if (strcmp(fOurSubsession.codecName(), "PCMU") == 0) {
            fWAVCodecTag = 0x0007;
            fAVIScale = fAVISize = numChannels; // 1 byte/sample
            fAVIRate = fAVISize * fAVISamplingFrequency;
        } else if (strcmp(fOurSubsession.codecName(), "MPA") == 0) {
            fWAVCodecTag = 0x0050;
            fAVIScale = fAVISize = 1;
            fAVIRate = 0; // ??? #####
        } else {
            fWAVCodecTag = 0x0001; // ??? #####
            fAVIScale = fAVISize = 1;
            fAVIRate = 0; // ??? #####
        }
    } else { // unknown medium
        fAVISubsessionTag
                = fourChar('0' + subsessionIndex / 10, '0' + subsessionIndex % 10, '?', '?');
        fAVICodecHandlerType = 0;
        fAVIScale = fAVISize = 1;
        fAVIRate = 0; // ??? #####
    }
}

void AVISubsessionIOState::afterGettingFrame(unsigned packetDataSize,
                                             struct timeval presentationTime) {
    // Begin by checking whether there was a gap in the RTP stream.
    // If so, try to compensate for this (if desired):
    unsigned short rtpSeqNum
            = fOurSubsession.rtpSource()->curPacketRTPSeqNum();
    if (fOurSink.fPacketLossCompensate && fPrevBuffer->bytesInUse() > 0) {
        short seqNumGap = rtpSeqNum - fLastPacketRTPSeqNum;
        for (short i = 1; i < seqNumGap; ++i) {
            // Insert a copy of the previous frame, to compensate for the loss:
            useFrame(*fPrevBuffer);
        }
    }
    fLastPacketRTPSeqNum = rtpSeqNum;

    // Now, continue working with the frame that we just got
    if (fBuffer->bytesInUse() == 0) {
        fBuffer->setPresentationTime(presentationTime);
    }
    fBuffer->addBytes(packetDataSize);

    useFrame(*fBuffer);
    if (fOurSink.fPacketLossCompensate) {
        // Save this frame, in case we need it for recovery:
        SubsessionBuffer *tmp = fPrevBuffer; // assert: != NULL
        fPrevBuffer = fBuffer;
        fBuffer = tmp;
    }
    fBuffer->reset(); // for the next input

    // Now, try getting more frames:
    fOurSink.continuePlaying();
}

void AVISubsessionIOState::useFrame(SubsessionBuffer &buffer) {
    unsigned char *const frameSource = buffer.dataStart();
    unsigned const frameSize = buffer.bytesInUse();
    struct timeval const &presentationTime = buffer.presentationTime();
    if (fPrevPresentationTime.tv_usec != 0 || fPrevPresentationTime.tv_sec != 0) {
        int uSecondsDiff
                = (presentationTime.tv_sec - fPrevPresentationTime.tv_sec) * 1000000
                  + (presentationTime.tv_usec - fPrevPresentationTime.tv_usec);
        if (uSecondsDiff > 0) {
            unsigned bytesPerSecond = (unsigned) ((frameSize * 1000000.0) / uSecondsDiff);
            if (bytesPerSecond > fMaxBytesPerSecond) {
                fMaxBytesPerSecond = bytesPerSecond;
            }
        }
    }
    fPrevPresentationTime = presentationTime;

    if (fIsByteSwappedAudio) {
        // We need to swap the 16-bit audio samples from big-endian
        // to little-endian order, before writing them to a file:
        for (unsigned i = 0; i < frameSize; i += 2) {
            unsigned char tmp = frameSource[i];
            frameSource[i] = frameSource[i + 1];
            frameSource[i + 1] = tmp;
        }
    }

    // Add an index record for this frame:
    AVIIndexRecord *newIndexRecord
            = new AVIIndexRecord(fAVISubsessionTag, // chunk id
                                 AVIIF_KEYFRAME, // flags
                                 4 + fOurSink.fNumBytesWritten, // offset (note: 4 == 'movi')
                                 frameSize); // size
    fOurSink.addIndexRecord(newIndexRecord);

    // Write the data into the file:
    fOurSink.fNumBytesWritten += fOurSink.addWord(fAVISubsessionTag);
    if (strcmp(fOurSubsession.codecName(), "H264") == 0) {
        // Insert a 'start code' (0x00 0x00 0x00 0x01) in front of the frame:
        fOurSink.fNumBytesWritten += fOurSink.addWord(4 + frameSize);
        fOurSink.fNumBytesWritten += fOurSink.addWord(
                fourChar(0x00, 0x00, 0x00, 0x01));//add start code
    } else {
        fOurSink.fNumBytesWritten += fOurSink.addWord(frameSize);
    }
    fwrite(frameSource, 1, frameSize, fOurSink.fOutFid);
    fOurSink.fNumBytesWritten += frameSize;
    // Pad to an even length:
    if (frameSize % 2 != 0) fOurSink.fNumBytesWritten += fOurSink.addByte(0);

    ++fNumFrames;
}

void AVISubsessionIOState::onSourceClosure() {
    fOurSourceIsActive = False;
    fOurSink.onSourceClosure1();
}


////////// AVI-specific implementation //////////

unsigned AVIFileSink::addWord(unsigned word) {
    // Add "word" to the file in little-endian order:
    addByte(word);
    addByte(word >> 8);
    addByte(word >> 16);
    addByte(word >> 24);

    return 4;
}

unsigned AVIFileSink::addHalfWord(unsigned short halfWord) {
    // Add "halfWord" to the file in little-endian order:
    addByte((unsigned char) halfWord);
    addByte((unsigned char) (halfWord >> 8));

    return 2;
}

unsigned AVIFileSink::addZeroWords(unsigned numWords) {
    for (unsigned i = 0; i < numWords; ++i) {
        addWord(0);
    }

    return numWords * 4;
}

unsigned AVIFileSink::add4ByteString(char const *str) {
    addByte(str[0]);
    addByte(str[1]);
    addByte(str[2]);
    addByte(str[3] == '\0' ? ' ' : str[3]); // e.g., for "AVI "

    return 4;
}

void AVIFileSink::setWord(unsigned filePosn, unsigned size) {
    do {
        if (SeekFile64(fOutFid, filePosn, SEEK_SET) < 0) break;
        addWord(size);
        if (SeekFile64(fOutFid, 0, SEEK_END) < 0) break; // go back to where we were

        return;
    } while (0);

    // One of the SeekFile64()s failed, probable because we're not a seekable file
    envir() << "AVIFileSink::setWord(): SeekFile64 failed (err "
            << envir().getErrno() << ")\n";
}

// Methods for writing particular file headers.  Note the following macros:

#define addFileHeader(tag, name) \
    unsigned AVIFileSink::addFileHeader_##name() { \
        add4ByteString("" #tag ""); \
        unsigned headerSizePosn = (unsigned)TellFile64(fOutFid); addWord(0); \
        add4ByteString("" #name ""); \
        unsigned ignoredSize = 8;/*don't include size of tag or size fields*/ \
        unsigned size = 12

#define addFileHeader1(name) \
    unsigned AVIFileSink::addFileHeader_##name() { \
        add4ByteString("" #name ""); \
        unsigned headerSizePosn = (unsigned)TellFile64(fOutFid); addWord(0); \
        unsigned ignoredSize = 8;/*don't include size of name or size fields*/ \
        unsigned size = 8

#define addFileHeaderEnd \
  setWord(headerSizePosn, size-ignoredSize); \
  return size; \
}

addFileHeader(RIFF, AVI);
    size += addFileHeader_hdrl();
    size += addFileHeader_movi();
    fRIFFSizePosition = headerSizePosn;
    fRIFFSizeValue = size - ignoredSize;
addFileHeaderEnd;

addFileHeader(LIST, hdrl);
    size += addFileHeader_avih();

    // Then, add a "strl" header for each subsession (stream):
    // (Make the video subsession (if any) come before the audio subsession.)
    unsigned subsessionCount = 0;
    MediaSubsessionIterator iter(fInputSession);
    MediaSubsession *subsession;
    while ((subsession = iter.next()) != NULL) {
        fCurrentIOState = (AVISubsessionIOState *) (subsession->miscPtr);
        if (fCurrentIOState == NULL) continue;
        if (strcmp(subsession->mediumName(), "video") != 0) continue;

        fCurrentIOState->setAVIstate(subsessionCount++);
        size += addFileHeader_strl();
    }
    iter.reset();
    while ((subsession = iter.next()) != NULL) {
        fCurrentIOState = (AVISubsessionIOState *) (subsession->miscPtr);
        if (fCurrentIOState == NULL) continue;
        if (strcmp(subsession->mediumName(), "video") == 0) continue;

        fCurrentIOState->setAVIstate(subsessionCount++);
        size += addFileHeader_strl();
    }

    // Then add another JUNK entry
    ++fJunkNumber;
    size += addFileHeader_JUNK();
addFileHeaderEnd;

#define AVIF_HASINDEX           0x00000010 // Index at end of file?
#define AVIF_MUSTUSEINDEX       0x00000020
#define AVIF_ISINTERLEAVED      0x00000100
#define AVIF_TRUSTCKTYPE        0x00000800 // Use CKType to find key frames?
#define AVIF_WASCAPTUREFILE     0x00010000
#define AVIF_COPYRIGHTED        0x00020000

addFileHeader1(avih);
    unsigned usecPerFrame = fMovieFPS == 0 ? 0 : 1000000 / fMovieFPS;
    size += addWord(usecPerFrame); // dwMicroSecPerFrame
    fAVIHMaxBytesPerSecondPosition = (unsigned) TellFile64(fOutFid);
    size += addWord(0); // dwMaxBytesPerSec (fill in later)
    size += addWord(0); // dwPaddingGranularity
    size += addWord(AVIF_TRUSTCKTYPE | AVIF_HASINDEX | AVIF_ISINTERLEAVED); // dwFlags
    fAVIHFrameCountPosition = (unsigned) TellFile64(fOutFid);
    size += addWord(0); // dwTotalFrames (fill in later)
    size += addWord(0); // dwInitialFrame
    size += addWord(fNumSubsessions); // dwStreams
    size += addWord(fBufferSize); // dwSuggestedBufferSize
    size += addWord(fMovieWidth); // dwWidth
    size += addWord(fMovieHeight); // dwHeight
    size += addZeroWords(4); // dwReserved
addFileHeaderEnd;

addFileHeader(LIST, strl);
    size += addFileHeader_strh();
    size += addFileHeader_strf();
    fJunkNumber = 0;
    size += addFileHeader_JUNK();
addFileHeaderEnd;

addFileHeader1(strh);
    size += add4ByteString(fCurrentIOState->fIsVideo ? "vids" :
                           fCurrentIOState->fIsAudio ? "auds" :
                           "????"); // fccType
    size += addWord(fCurrentIOState->fAVICodecHandlerType); // fccHandler
    size += addWord(0); // dwFlags
    size += addWord(0); // wPriority + wLanguage
    size += addWord(0); // dwInitialFrames
    size += addWord(fCurrentIOState->fAVIScale); // dwScale
    size += addWord(fCurrentIOState->fAVIRate); // dwRate
    size += addWord(0); // dwStart
    fCurrentIOState->fSTRHFrameCountPosition = (unsigned) TellFile64(fOutFid);
    size += addWord(0); // dwLength (fill in later)
    size += addWord(fBufferSize); // dwSuggestedBufferSize
    size += addWord((unsigned) -1); // dwQuality
    size += addWord(fCurrentIOState->fAVISize); // dwSampleSize
    size += addWord(0); // rcFrame (start)
    if (fCurrentIOState->fIsVideo) {
        size += addHalfWord(fMovieWidth);
        size += addHalfWord(fMovieHeight);
    } else {
        size += addWord(0);
    }
addFileHeaderEnd;

addFileHeader1(strf);
    if (fCurrentIOState->fIsVideo) {
        // Add a BITMAPINFO header:
        unsigned extraDataSize = 0;
        size += addWord(10 * 4 + extraDataSize); // size
        size += addWord(fMovieWidth);
        size += addWord(fMovieHeight);
        size += addHalfWord(1); // planes
        size += addHalfWord(24); // bits-per-sample #####
        size += addWord(fCurrentIOState->fAVICodecHandlerType); // compr. type
        size += addWord(fCurrentIOState->fAVISize);
        size += addZeroWords(4); // ??? #####
        // Later, add extra data here (if any) #####
    } else if (fCurrentIOState->fIsAudio) {
        // Add a WAVFORMATEX header:
        size += addHalfWord(fCurrentIOState->fWAVCodecTag);
        unsigned numChannels = fCurrentIOState->fOurSubsession.numChannels();
        size += addHalfWord(numChannels);
        size += addWord(fCurrentIOState->fAVISamplingFrequency);
        size += addWord(fCurrentIOState->fAVIRate); // bytes per second
        size += addHalfWord(fCurrentIOState->fAVISize); // block alignment
        unsigned bitsPerSample = (fCurrentIOState->fAVISize * 8) / numChannels;
        size += addHalfWord(bitsPerSample);
        if (strcmp(fCurrentIOState->fOurSubsession.codecName(), "MPA") == 0) {
            // Assume MPEG layer II audio (not MP3): #####
            size += addHalfWord(22); // wav_extra_size
            size += addHalfWord(2); // fwHeadLayer
            size += addWord(8 * fCurrentIOState->fAVIRate); // dwHeadBitrate #####
            size += addHalfWord(numChannels == 2 ? 1 : 8); // fwHeadMode
            size += addHalfWord(0); // fwHeadModeExt
            size += addHalfWord(1); // wHeadEmphasis
            size += addHalfWord(16); // fwHeadFlags
            size += addWord(0); // dwPTSLow
            size += addWord(0); // dwPTSHigh
        }
    }
addFileHeaderEnd;

#define AVI_MASTER_INDEX_SIZE   256

addFileHeader1(JUNK);
    if (fJunkNumber == 0) {
        size += addHalfWord(4); // wLongsPerEntry
        size += addHalfWord(0); // bIndexSubType + bIndexType
        size += addWord(0); // nEntriesInUse #####
        size += addWord(fCurrentIOState->fAVISubsessionTag); // dwChunkId
        size += addZeroWords(2); // dwReserved
        size += addZeroWords(AVI_MASTER_INDEX_SIZE * 4);
    } else {
        size += add4ByteString("odml");
        size += add4ByteString("dmlh");
        unsigned wtfCount = 248;
        size += addWord(wtfCount); // ??? #####
        size += addZeroWords(wtfCount / 4);
    }
addFileHeaderEnd;

addFileHeader(LIST, movi);
    fMoviSizePosition = headerSizePosn;
    fMoviSizeValue = size - ignoredSize;
addFileHeaderEnd;
